/*
 * Copyright (C) 2010-2011 sunjumper@163.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package mf.mfrpc.servlet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import mf.mfrpc.http.HttpConstants;
import mf.mfrpc.http.HttpException;
import mf.mfrpc.http.HttpParser;
import mf.mfrpc.http.TransferType;
import mf.mfrpc.parser.KryoParser;
import mf.mfrpc.serialize.DataSource;
import mf.mfrpc.serialize.Parser;
import mf.mfrpc.util.Base64;
import mfinder.ActionFactory;
import mfinder.MFinderException;
import mfinder.config.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 通过此filter处理http请求，并返回相应的相应信息。
 */
public class MFrpcFilter implements Filter {

    /** log */
    private static final Logger LOG = LoggerFactory.getLogger(MFrpcFilter.class);

    /** default encoding */
    private String encoding = "UTF-8";

    /** default config file */
    private String config = "MFrpc.xml";

    /** default no need http */
    private boolean httpRequired = true;

    /** 返回客户端错误堆栈信息的深度 */
    private int exceptionMessagesDepth = 3;

    /** action工厂 */
    private ActionFactory factory;

    /** http解析器 */
    private HttpParser parser;

    /** 空行符 */
    private String lineSeparator = System.getProperty("line.separator");

    @Override
    public void init(FilterConfig filterConfig) {
        String encode = filterConfig.getInitParameter("encoding");
        String conf = filterConfig.getInitParameter("config");
        String factoryName = filterConfig.getInitParameter("factoryName");
        String parserClass = filterConfig.getInitParameter("parserClass");
        String required = filterConfig.getInitParameter("httpRequired");
        String exceptionMessages = filterConfig.getInitParameter("exceptionMessagesDepth");
        if (null != encode)
            this.encoding = encode;
        if (null != conf)
            this.config = conf;

        if (null != required)
            httpRequired = Boolean.parseBoolean(required);
        if (null != exceptionMessages)
            exceptionMessagesDepth = Integer.parseInt(exceptionMessages);

        //create ActionFactory
        createActionFactory(filterConfig);

        if (factoryName != null)
            filterConfig.getServletContext().setAttribute(factoryName, factory);
        try {
            parser = new HttpParser(parserClass == null ? new KryoParser() : (Parser) Class.forName(parserClass).newInstance());
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * a hook to give subclass another way to create ActionFactory。
     *
     * @param filterConfig 过滤器配置。
     */
    protected void createActionFactory(FilterConfig filterConfig) {
        Configuration configuration = new Configuration();
        configuration.load(this.getClass().getClassLoader().getResourceAsStream(config));
        factory = configuration.getFactory();
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws
            ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        request.setCharacterEncoding(encoding);
        response.setCharacterEncoding(encoding);

        //must be multipart request
        if (HttpParser.isMultipart(request)) {
            try {
                //set http request/response/servletContext
                if (httpRequired)
                    createActionContext(request, response);

                String url = findAction(request);
                Object result = factory.invokeAction(url, parser.parse(request));
                LOG.debug("[{}] result : {}", url, result);
                if (result != null) {

                    //组装result结果
                    //处理返回流类型
                    if (result instanceof InputStream) {
                        setTransferType(response, TransferType.STREAM);
                        streamTransfer(response, (InputStream) result);
                    } else if (result instanceof DataSource) {
                        DataSource ds = (DataSource) result;
                        ByteArrayOutputStream bout = new ByteArrayOutputStream(128);
                        parser.getParser().writeObject(bout, ds);

                        //set the serialized part(properties) to header
                        response.setHeader(HttpConstants.DATASOURCE_INFORMATION_HEADER, Base64.encodeBytes(bout.toByteArray()));

                        setTransferType(response, TransferType.DATASOURCE);
                        streamTransfer(response, ds.getInputStream());
                    } else {
                        setTransferType(response, TransferType.OBJECT);
                        parser.getParser().writeObject(response.getOutputStream(), result);
                    }
                }
            } catch (Exception e) {
                //handle the exception message to client
                exceptionHandler(response, e);
                throw new HttpException(e);
            } finally {
                if (httpRequired)
                    ServletThreadContext.remove();
            }
        }
        if (!response.isCommitted())
            chain.doFilter(request, response);
    }

    //find the action url
    private static String findAction(HttpServletRequest request) {
        return request.getServletPath();
    }

    @Override
    public void destroy() {
        if (factory != null) {
            factory.clear();
        }
    }

    //put request & response
    private void createActionContext(HttpServletRequest request, HttpServletResponse response) {
        if (ServletThreadContext.get() == null)
            ServletThreadContext.set(new ServletThreadContext(new HashMap<String, Object>()));
        ServletThreadContext.setRequest(request);
        ServletThreadContext.setResponse(response);
        ServletThreadContext.setServletContext(request.getSession().getServletContext());
    }

    /**
     * 流传输。
     */
    private static void streamTransfer(ServletResponse response, InputStream inputStream) throws
            IOException {
        if (inputStream == null)
            return;
        OutputStream output = null;
        try {
            output = response.getOutputStream();
            byte[] oBuff = new byte[4096];
            int iSize;
            while (-1 != (iSize = inputStream.read(oBuff))) {
                output.write(oBuff, 0, iSize);
            }
            output.flush();
        } finally {
            if (inputStream != null)
                inputStream.close();
            if (output != null)
                output.close();
        }
    }

    /**
     * 设置传输类型。
     */
    private static void setTransferType(HttpServletResponse response, TransferType tt) {
        response.setHeader(HttpConstants.RESPONSE_TRANSFERTYPE_HEADER, tt.name());
    }

    /**
     * 异常信息处理。
     *
     * @param response http响应。
     * @param ex 异常。
     */
    private void exceptionHandler(HttpServletResponse response, Exception ex) throws IOException {
        //ignore the InvokeException
        //exception occurred in Action should be handled in exception interceptor/intercepror stack
        //set the response code 500 and give the exception messages to client.
        Throwable real = ex instanceof MFinderException ? ex.getCause() : ex;
        StringBuilder errMsg = new StringBuilder(real.toString());
        errMsg.append(lineSeparator);

        //the source exception
        Throwable c = real;
        //temp variable to find the source excpetion
        Throwable t = real;
        while ((t = t.getCause()) != null) {
            c = t;
        }
        if (c != real)
            errMsg.append("Caused by: ").append(c.toString()).append(lineSeparator);
        StackTraceElement[] s = c.getStackTrace();
        if (s != null) {
            for (int i = 0; i < exceptionMessagesDepth && i < s.length; i++) {
                errMsg.append("\tat ");
                errMsg.append(s[i].getFileName()).
                        append(':').append(s[i].getLineNumber());
                errMsg.append(lineSeparator);
            }
        }
        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Base64.encodeBytes(errMsg.toString().getBytes(encoding)));
    }
}
